#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Handles login/logout pages and dealing with user cookies.

Includes a WSGI application that serves the login page and handles login and
logout HTTP requests. It accepts these GET query parameters:

  continue: URL to redirect to after a login or logout has completed.
  email: Email address to set for the client.
  admin: If 'True', the client should be logged in as an admin.
  action: What action to take ('Login' or 'Logout').

To view the current user information and a form for logging in and out,
supply no parameters.
"""


import cgi
import Cookie
import hashlib
import logging
import os
import sha
import sys
import urllib
import uuid

import webapp2

app_dashboard_lib = '/../../../../../AppDashboard/lib'
sys.path.append(os.path.dirname(__file__) + app_dashboard_lib)
from app_dashboard_helper import AppDashboardHelper

# URL of the login page within the dev appserver.
LOGIN_URL_RELATIVE = '_ah/login'

# CGI parameter constants.
CONTINUE_PARAM = 'continue'
_EMAIL_PARAM = 'email'
_ADMIN_PARAM = 'admin'
ACTION_PARAM = 'action'

# Values for the action parameter.
LOGOUT_ACTION = 'logout'
LOGIN_ACTION = 'login'

# Name of the cookie that stores the user info.
_COOKIE_NAME = 'dev_appserver_login'

# Indicates that the user has admin access to all applications.
CLOUD_ADMIN_MARKER = 'CLOUD_ADMIN'

# The port that the AppDashboard serves HTTPS traffic on.
DASHBOARD_HTTPS_PORT = "1443"


def get_user_info(http_cookie, cookie_name=_COOKIE_NAME):
  """Gets the requestor's user info from an HTTP Cookie header.

  Args:
    http_cookie: The value of the 'Cookie' HTTP request header.
    cookie_name: The name of the cookie that stores the user info.

  Returns:
    A tuple (email, admin, user_id) where:
      email: The user's email address, if any.
      admin: True if the user is an admin; False otherwise.
      user_id: The user ID, if any.
  """
  try:
    cookie = Cookie.SimpleCookie(http_cookie)
  except Cookie.CookieError:
    return '', False, ''

  cookie_dict = dict((k, v.value) for k, v in cookie.iteritems())
  return _get_user_info_from_dict(cookie_dict, cookie_name)


def _get_user_info_from_dict(cookie_dict, cookie_name=_COOKIE_NAME):
  """Gets the requestor's user info from a cookie dictionary.

  Args:
    cookie_dict: A dictionary mapping cookie names onto values.
    cookie_name: The name of the cookie that stores the user info.

  Returns:
    A tuple (email, admin, user_id) where:
      email: The user's email address, if any.
      admin: True if the user is an admin; False otherwise.
      user_id: The user ID, if any.
  """
  cookie_secret = os.environ['COOKIE_SECRET']

  cookie_value = cookie_dict.get(cookie_name, '')
  cookie_value = cookie_value.replace("%3A",":")
  cookie_value = cookie_value.replace("%40",'@')
  cookie_value = cookie_value.replace("%2C",",")

  email, nickname, admin, hsh = (cookie_value.split(':') + ['', '', '', ''])[:4]

  if email == '':
    nickname = ''
    admin = ''
    return '', False, ''
  else:
    vhsh = sha.new(email+nickname+admin+cookie_secret).hexdigest()
    if hsh != vhsh:
      logging.info("{0} has an invalid cookie, so ignoring it.".format(email))
      return '', False, ''

  admin_apps = admin.split(',')
  current_app = os.environ['APPLICATION_ID']
  is_admin = current_app in admin_apps or CLOUD_ADMIN_MARKER in admin_apps
  return email, is_admin, nickname

def _create_cookie_data(email, admin):
  """Creates cookie payload data.

  Args:
    email: The user's email address.
    admin: True if the user is an admin; False otherwise.

  Returns:
    A string containing the cookie payload.
  """
  if email:
    user_id_digest = hashlib.md5(email.lower()).digest()
    user_id = '1' + ''.join(['%02d' % ord(x) for x in user_id_digest])[:20]
  else:
    user_id = ''
  return '%s:%s:%s' % (email, admin, user_id)


def _set_user_info_cookie(email, admin, cookie_name=_COOKIE_NAME):
  """Creates a cookie to set the user information for the requestor.

  Args:
    email: The email to set for the user.
    admin: True if the user should be admin; False otherwise.
    cookie_name: The name of the cookie that stores the user info.

  Returns:
    Set-Cookie value for setting the user info of the requestor.
  """
  cookie_value = _create_cookie_data(email, admin)
  cookie = Cookie.SimpleCookie()
  cookie[cookie_name] = cookie_value
  cookie[cookie_name]['path'] = '/'
  return cookie[cookie_name].OutputString()


def _clear_user_info_cookie(cookie_name=_COOKIE_NAME):
  """Clears the user info cookie from the requestor, logging them out.

  Args:
    cookie_name: The name of the cookie that stores the user info.

  Returns:
    A Set-Cookie value for clearing the user info of the requestor.
  """
  cookie = Cookie.SimpleCookie()
  cookie[cookie_name] = ''
  cookie[cookie_name]['path'] = '/'
  cookie[cookie_name]['max-age'] = '0'
  if AppDashboardHelper.USE_SHIBBOLETH:
    cookie[cookie_name]['domain'] = AppDashboardHelper.\
      SHIBBOLETH_COOKIE_DOMAIN
  return cookie[cookie_name].OutputString()


_LOGIN_TEMPLATE = """<html>
<head>
  <title>Login</title>
</head>
<body>

<form method="get" action="%(login_url)s"
      style="text-align:center; font: 13px sans-serif">
  <div style="width: 20em; margin: 1em auto;
              text-align:left;
              padding: 0 2em 1.25em 2em;
              background-color: #d6e9f8;
              border: 2px solid #67a7e3">
    <h3>%(login_message)s</h3>
    <p style="padding: 0; margin: 0">
      <label for="email" style="width: 3em">Email:</label>
      <input name="email" type="email" value="%(email)s" id="email"/>
    </p>
    <p style="margin: .5em 0 0 3em; font-size:12px">
      <input name="admin" type="checkbox" value="True"
       %(admin_checked)s id="admin"/>
        <label for="admin">Sign in as Administrator</label>
    </p>
    <p style="margin-left: 3em">
      <input name="action" value="Login" type="submit"
             id="submit-login" />
      <input name="action" value="Logout" type="submit"
             id="submit-logout" />
    </p>
  </div>
  <input name="continue" type="hidden" value="%(continue_url)s"/>
</form>

</body>
</html>
"""


def _render_login_template(login_url, continue_url, email, admin):
  """Renders the login page.

  Args:
    login_url: The parameter to _login_response.
    continue_url: The parameter to _login_response.
    email: The email address of the current user, if any.
    admin: True if the user is currently an admin; False otherwise.

  Returns:
    A string containing the contents of the login page.
  """
  if email:
    login_message = 'Logged in'
  else:
    login_message = 'Not logged in'

    email = 'test\x40example.com'
  admin_checked = 'checked' if admin else ''

  template_dict = {
      'email': cgi.escape(email, quote=True),
      'admin_checked': admin_checked,
      'login_message': login_message,
      'login_url': cgi.escape(login_url, quote=True),
      'continue_url': cgi.escape(continue_url, quote=True),
  }

  return _LOGIN_TEMPLATE % template_dict


def login_redirect(application_url, continue_url, start_response):
  """Writes a login redirection URL to a user.

  This redirects to login_url with a continue parameter to return to
  continue_url. The login_url should be on the canonical front-end server,
  regardless of the host:port the user connected to.

  Args:
    application_url: The URL of the dev appserver domain
      (e.g., 'http://localhost:8080').
    continue_url: The URL to continue to after the user logs in.
    start_response: A WSGI start_response function.

  Returns:
    An (empty) iterable over strings containing the body of the HTTP response.
  """
  if AppDashboardHelper.USE_SHIBBOLETH:
    redirect_url = '{0}:{1}/login?{2}={3}'.format(
      AppDashboardHelper.SHIBBOLETH_CONNECTOR,
      AppDashboardHelper.SHIBBOLETH_CONNECTOR_PORT,
      CONTINUE_PARAM,
      urllib.quote(continue_url)
    )
  else:
    hostname = os.environ['NGINX_HOST']
    redirect_url = 'https://{0}:{1}/login?{2}={3}'.format(
      hostname,
      DASHBOARD_HTTPS_PORT,
      CONTINUE_PARAM,
      urllib.quote(continue_url))

  start_response('302 Requires login',
                 [('Location', redirect_url)])
  return []


def fake_admin():
  """ Generate the fake admin login secret

  Returns:
    A string containing the fake login secret
  """
  return hashlib.sha1('{}/{}'.format(
      os.environ.get('APPNAME', str(uuid.uuid4())),
      os.environ.get('COOKIE_SECRET', str(uuid.uuid4())))).hexdigest()


class Handler(webapp2.RequestHandler):
  """The request handler for the login and logout pages."""

  def get(self):
    action = self.request.get(ACTION_PARAM)
    set_email = self.request.get(_EMAIL_PARAM)
    set_admin = self.request.get(_ADMIN_PARAM).lower() == 'true'
    continue_url = self.request.get(CONTINUE_PARAM)

    login_url = self.request.path_url

    if action:
      redirect_url = continue_url or login_url

      # Perform the action.
      if action.lower() == LOGOUT_ACTION.lower():
        self.response.headers['Set-Cookie'] = _clear_user_info_cookie()
        if AppDashboardHelper.USE_SHIBBOLETH:
          redirect_url = AppDashboardHelper.SHIBBOLETH_LOGOUT_URL
      elif action.lower() == LOGIN_ACTION.lower() and set_email:
        self.response.headers['Set-Cookie'] = _set_user_info_cookie(set_email,
                                                                    set_admin)

      # URLs should be ASCII-only byte strings.
      if isinstance(redirect_url, unicode):
        redirect_url = redirect_url.encode('ascii')

      # Redirect the user after performing the action.
      self.response.status = 302
      self.response.status_message = 'Redirecting to continue URL'
      self.response.headers['Location'] = redirect_url
    else:
      # Send the user to the AppDashboard to log in before letting them view the
      # specified URL.
      if AppDashboardHelper.USE_SHIBBOLETH:
        appscale_login_url = "{0}:{1}/login".format(
          AppDashboardHelper.SHIBBOLETH_CONNECTOR, DASHBOARD_HTTPS_PORT)
      else:
        appscale_login_url = "https://{0}:{1}/login".format(
          os.environ['NGINX_HOST'], DASHBOARD_HTTPS_PORT)
      redirect_url = '{0}?{1}={2}'.format(appscale_login_url, CONTINUE_PARAM,
        continue_url)
      self.response.status = 302
      self.response.status_message = 'Redirecting to login service URL'
      self.response.headers['Location'] = redirect_url


application = webapp2.WSGIApplication([('/.*', Handler)], debug=True)
